package io.capsulode.tunnel.blockrepo.pac;

import io.capsulode.tunnel.blockrepo.PacConfiguration;
import io.capsulode.tunnel.blockrepo.PacRepository;
import io.capsulode.tunnel.blockrepo.util.StringUtils;
import io.capsulode.tunnel.blockrepo.util.WebUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Stream;

@Service
public class BlacklistOrWhitelistPacRepository implements PacRepository {
    private final Logger logger = LoggerFactory.getLogger(BlacklistOrWhitelistPacRepository.class);
    private String etag;
    private long lastModified;
    private List<String> pacEndingLines;

    private volatile boolean changed = false;
    private Set<String> blocks;
    private final Mode mode;

    public BlacklistOrWhitelistPacRepository(PacConfiguration pacConfiguration) {
        this.mode = pacConfiguration.getMode();
    }

    @Override
    public boolean put(String block) {
        boolean success = blocks.add(block);
        if (success) {
            changed = true;
            setModified(System.currentTimeMillis());
        }
        return success;
    }

    @Override
    public boolean remove(String block) {
        boolean success = blocks.remove(block);
        if (success) {
            changed = true;
            setModified(System.currentTimeMillis());
        }
        return success;
    }

    @Override
    public Set<String> domains() {
        return blocks;
    }

    @Override
    public String etag() {
        return etag;
    }

    @Override
    public long lastModified() {
        return lastModified;
    }

    @Override
    public List<String> getPacEndingLines() {
        return pacEndingLines;
    }

    private void setModified(long lastModified) {
        this.lastModified = lastModified;
        ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
        buffer.putLong(lastModified);

        etag = DigestUtils.md5DigestAsHex(buffer.array());
    }

    private void initPacEndingLines() throws IOException {
        pacEndingLines = new ArrayList<>();
        String templateLocation = String.format("/templates/pac-%s.js", mode.name().toLowerCase());
        try (InputStream resourceAsStream = getClass().getResourceAsStream(templateLocation)) {
            if (resourceAsStream == null) {
                logger.warn("Template not found: {}", templateLocation);
                return;
            }
            BufferedReader reader = new BufferedReader(new InputStreamReader(resourceAsStream));
            String line;
            while ((line = reader.readLine()) != null) {
                pacEndingLines.add(line);
            }
        }
    }

    @PostConstruct
    private void startup() throws IOException {
        initPacEndingLines();

        Path dbPath = getStorage();
        if (!Files.exists(dbPath)) {
            Files.createFile(dbPath);
            String defaultDb = String.format("https://gitee.com/terrason/tunnel/raw/master/block-repo/pac-%s.db",
                    mode.name().toLowerCase());
            WebUtil.download(defaultDb, dbPath);
            changed = true;
        }

        blocks = new TreeSet<>();
        try (Stream<String> lines = Files.lines(dbPath)) {
            lines.filter(StringUtils::isNotBlank)
                    .forEach(blocks::add);
        }
        setModified(System.currentTimeMillis());

        logger.info("PAC Repository running at {} mode.", mode);
    }

    @PreDestroy
    private void shutdown() {
        sync();
    }

    @Scheduled(fixedRate = 60000)
    private void sync() {
        if (!changed) {
            return;
        }
        logger.debug("Saving changes!");
        Path dbPath = getStorage();
        try {
            if (!Files.exists(dbPath)) {
                Files.createFile(dbPath);
            }
            Files.write(dbPath, blocks, StandardCharsets.UTF_8);
            changed = false;
        } catch (IOException ex) {
            logger.warn("Failed to write to storage: {}", dbPath.toAbsolutePath(), ex);
        }
    }

    private Path getStorage() {
        Path dbPath = Paths.get(String.format("pac-%s.db", mode.name().toLowerCase()));
        if (logger.isDebugEnabled()) {
            logger.debug("Use storage: {}", dbPath.toAbsolutePath());
        }
        return dbPath;
    }
}
